﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.IO;

namespace SecurEntityLib
{
    public abstract class SecurEntity
    {
        public virtual string SecurEntityData { get; set; }

        public virtual Guid SecurEntityID { get; set; }

        public virtual byte[] SecurEntityThumbprint { get; set; }

        public abstract string GetPrimaryKeyPropertyName();

        private bool _IsPropertyForEncryption(PropertyInfo pi)
        {
            object o = pi.GetValue(this, null);
            if (o is string)
            {
                return true;
            }

            return false;
        }

        private bool _IsPropertyForIntegrity(
            PropertyInfo pi,
            out bool isPlainStringType)
        {
            isPlainStringType = false;

            if (0 == String.Compare(pi.Name, "SecurEntityID", true))
            {
                return true;
            }

            if (true == pi.Name.EndsWith(
                "ID", StringComparison.InvariantCultureIgnoreCase))
            {
                return false;
            }

            if (0 == String.Compare(pi.Name, "SecurEntityData", true))
            {
                return false;
            }

            if (0 == String.Compare(pi.Name, "SecurEntityThumbprint", true))
            {
                return true;
            }

            if (0 == String.Compare(
                pi.Name, GetPrimaryKeyPropertyName(), true))
            {
                return false;
            }

            if (true == pi.PropertyType.IsGenericType)
            {
                return false;
            }

            object o = pi.GetValue(this, null);
            if (o is DateTime ||
                o is string ||
                o is int ||
                o is uint ||
                o is SecurEntity)
            {
                if (o is string)
                    isPlainStringType = true;
                return true;
            }

            return false;
        }

        private static int ComparePropertyInfos(PropertyInfo pi1, PropertyInfo pi2)
        {
            return String.Compare(pi1.Name, pi2.Name);
        }

        private static void _CheckSecurEntityID(bool read, SecurEntity se)
        {
            if (false == read)
            {
                int pk = (int)se.GetType().GetProperty(
                    se.GetPrimaryKeyPropertyName()).GetValue(se, null);

                if (0 == pk)
                {
                    //
                    // N.B. - this library assumes that the Primary Key of any 
                    // SecurEntity is of type int. However, since that value
                    // is generated automatically by SQL the first time the 
                    // object/row is written, it can't be used in the hash
                    // computation. 
                    //
                    // Instead, this GUID property is used in order to support
                    // the cryptographic binding of relationships between 
                    // SecurEntity objects.
                    //

                    if (null == se.SecurEntityID ||
                        0 == se.SecurEntityID.CompareTo(new Guid()))
                    {
                        se.SecurEntityID = Guid.NewGuid();
                    }
                }
            }
            else if (null == se.SecurEntityID ||
                0 == se.SecurEntityID.CompareTo(new Guid()))
            {
                throw new Exception(
                    "SecurEntity integrity verification failed.");
            }
        }

        private void _AddProperties(
            bool read,
            ref CryptoHelper ch)
        {
            PropertyInfo[] sourcePropertyInfos = this.GetType().GetProperties(
                    BindingFlags.Instance | BindingFlags.Public);
            List<PropertyInfo> filteredPropertyInfos = new List<PropertyInfo>();
            string plaintext = null;
            string ciphertext = null;
            CryptoHelper.AddDataAction action = 
                CryptoHelper.AddDataAction.DoNothing;
            int stringTypePropertyCount = 0;
            bool colIsStringType = false; 

            //
            // Check if this appears to be a brand new object
            //

            _CheckSecurEntityID(read, this);

            //
            // Filter out the unsupported property types
            //

            for (int i = 0; i < sourcePropertyInfos.Length; i++)
            {
                if (_IsPropertyForIntegrity(
                    sourcePropertyInfos[i], out colIsStringType))
                {
                    filteredPropertyInfos.Add(sourcePropertyInfos[i]);
                }

                if (true == colIsStringType)
                {
                    stringTypePropertyCount++;
                    colIsStringType = false;
                }
            }

            //
            // Sort the filtered property list to ensure that the same field 
            // order is always used for the following cryptographic operations
            //

            filteredPropertyInfos.Sort(ComparePropertyInfos);

            //
            // If the row has no more than one column of type 'string', and 
            // the current operation is writing to the DB, assume
            // that it's useful to compute and store a keyed hash of the 
            // string column for faster lookup.
            //

            if (false == read)
            {
                if (1 == stringTypePropertyCount)
                {
                    string stringFieldValue = null;

                    //
                    // Find the string field value and index again
                    //

                    for (int i = 0; i < filteredPropertyInfos.Count; i++)
                    {
                        object o = filteredPropertyInfos[i].GetValue(this, null);
                        if (o is string)
                        {
                            stringFieldValue = (string)o;
                            break;
                        }
                    }

                    //
                    // Get the keyed hash value 
                    //

                    SecurEntityThumbprint = GetThumbprint(stringFieldValue);
                }
            }

            //
            // Iterate again through the supported property types
            //

            for (int i = 0; i < filteredPropertyInfos.Count; i++)
            {
                //
                // Turn the property into a byte array
                //

                MemoryStream ms = new MemoryStream();
                BinaryWriter bw = new BinaryWriter(ms);
                object o = filteredPropertyInfos[i].GetValue(this, null);
                if (o is SecurEntity)
                {
                    _CheckSecurEntityID(read, (SecurEntity)o);
                    bw.Write(((SecurEntity)o).SecurEntityID.ToString());
                }
                else
                {
                    bw.Write(null == o ? "" : o.ToString());
                }
                bw.Close();

                //
                // Determine whether to encrypt, decrypt, or neither
                //

                plaintext = null;
                ciphertext = null;
                if (true == _IsPropertyForEncryption(filteredPropertyInfos[i]))
                {
                    if (true == read)
                    {
                        action = CryptoHelper.AddDataAction.Decrypt;
                        ciphertext = (string)o;
                    }
                    else
                    {
                        action = CryptoHelper.AddDataAction.Encrypt;
                    }
                }
                else
                {
                    action = CryptoHelper.AddDataAction.DoNothing;
                }

                //
                // Perform the required crypto
                //

                ch.AddData(
                    ms.ToArray(),
                    action,
                    i == filteredPropertyInfos.Count - 1,
                    ref plaintext,
                    ref ciphertext);

                //
                // Set the encrypted or decrypted property, if appropriate
                //

                switch (action)
                {
                    case CryptoHelper.AddDataAction.Decrypt:
                        filteredPropertyInfos[i].SetValue(
                            this, plaintext, null);
                        break;
                    case CryptoHelper.AddDataAction.Encrypt:
                        filteredPropertyInfos[i].SetValue(
                            this, ciphertext, null);
                        break;
                    default:
                        break;
                }
            }
        }

        public void Encrypt()
        {
            StringBuilder log = new StringBuilder();
            CryptoHelper ch = 
                new CryptoHelper();
            ch.Initialize(SecretKeyStorage.Instance.SecretKey);

            _AddProperties(false, ref ch);
            SecurEntityData = ch.ToString();
        }

        public void Decrypt()
        {
            StringBuilder log = new StringBuilder();
            CryptoHelper ch =
                new CryptoHelper(
                    SecretKeyStorage.Instance.SecretKey, SecurEntityData);

            _AddProperties(true, ref ch);
            if (ch.Verify() == false)
            {
                throw new Exception( 
                    "SecurEntity integrity verification failed.");
            }
        }

        public byte[] GetThumbprint(string columnValue)
        {
            return CryptoHelper.GetStringColumnValueThumbprint(
                SecretKeyStorage.Instance.SecretKey,
                columnValue,
                GetPrimaryKeyPropertyName());
        }
    }
}
